#!/usr/bin/env bash

# Name:    sk-translit
# From:    <http://code.google.com/p/sk-translit/>
#
# DESCRIPTION:
#
# A simple Bash script that converts sanskrit text between
# several encoding schemes. Supported schemes are nāgarī,
# IAST, ITRANS, Harvard-Kyoto, and SLP1. The script also features
# a parsing option that permits to transliterate only selected
# portions of the input text.
#
# To make the script work, it is necessary to activate UTF-8,
# and to install some nāgarī unicode font.
#
# LICENSE:
#
# This program is free software: you can redistribute it
# and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any
# later version.
# It is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details:
# <http://www.gnu.org/licenses/>.



#----------------------------------------------------------#
#                 USER-DEFINED VARIABLES                   #
#----------------------------------------------------------#


INPUT_SCHEME=
OUTPUT_SCHEME=
DELIMITORS='<,>'



#----------------------------------------------------------#
#                   PROGRAM VARIABLES                      #
#----------------------------------------------------------#


SCRIPT_NAME="${0##*/}"

SCHEMES='NAG IAST ITRANS HK SLP1'



#----------------------------------------------------------#
#               TRANSLITERATION SCHEMES                    #
#----------------------------------------------------------#


IAST=(
a ā i ī u ū ṛ r̥̄ ḷ ḹ
e ai o au ṃ ḥ
k kh g gh ṅ
c ch j jh ñ
ṭ ṭh ḍ ḍh ṇ
t th d dh n
p ph b bh m
y r l v
ś ṣ s h
A Ā I Ī U Ū Ṛ Ṝ Ḷ Ḹ
E Ai O Au Ṃ Ḥ
K Kh G Gh Ṅ
C Ch J Jh Ñ
Ṭ Ṭh Ḍ Ḍh Ṇ
T Th D Dh N
P Ph B Bh M
Y R L V
Ś Ṣ S H
)

# References for Harvard-Kyoto scheme:
# http://www.sktutils.com/html/hk.html

HK=(
a A i I u U R RR lR lRR
e ai o au M H
k kh g gh G
c ch j jh J
T Th D Dh N
t th d dh n
p ph b bh m
y r l v
z S s h
\'
)

# References for SLP1 scheme:
# http://sanskrit1.ccv.brown.edu/tomcat/sl/-/pub/lies_sl.pdf (p. 171 sq. in a PDF reader)

SLP1=(
a A i I u U f F x X
e E o O M H
k K g G N
c C j J Y
w W q Q R
t T d D n
p P b B m
y r l v
S z s h
)


# References for ITRANS scheme:
# http://en.wikipedia.org/wiki/ITRANS
# http://www.aczoom.com/itrans/#itransencoding

ITRANS=(
a aa,A i ii,I u uu,U RRi,R^i RRI,R^I LLi,L^i LLI,L^I
e ai o au .n,M,.m H
k kh g gh ~N,N^
ch Ch j jh ~n,JN
T Th D Dh N
t th d dh n
p ph b bh m
y r l v,w
sh Sh,shh s h
ld,L x,kSh  GY,j~n,dny shr
.a
)

NAG=(
अ आ इ ई उ ऊ ऋ ॠ ऌ ॡ
ए ऐ ओ औ ं  ः
क ख ग घ ङ
च छ ज झ ञ
ट ठ ड ढ ण
त थ द ध न
प फ ब भ म
य र ल व
श ष स ह
ऽ  ा  ि  ी  ु  ू  ृ  ॄ  ॢ  ॣ
 े  ै  ो  ौ  ्
० १ २ ३ ४ ५ ६ ७ ८ ९
। ॥
)

XML=(
\" \&quot\;
\& \&amp\;
\' \&apos\;
\< \&lt\;
\> \&gt\;
)

ADD_SIGNS=(
\$
\' \‘
0 1 2 3 4 5 6 7 8 9
\| \|\|
)



#----------------------------------------------------------#
#               PROGRAM-RELATED FUNCTIONS                  #
#----------------------------------------------------------#


log_notice ()
{
# Prints some clever messages.
echo "$SCRIPT_NAME: $@" >&2
}


log_error ()
{
# Same as the above, but exits the program.
echo "$SCRIPT_NAME: $@" >&2
exit 1
}


usage ()
{
cat << EOF >&2
Usage: $SCRIPT_NAME [OPTIONS] [TEXT|FILE]

Transliterates sanskrit text between several encoding schemes.
Supported schemes are nāgarī, IAST, Harvard-Kyoto, ITRANS, and SLP1.

Options :
   -i, --input-scheme=INPUT_SCHEME		Defines the input scheme to use.
   -o, --output-scheme=OUTPUT_SCHEME		Defines the output scheme to use.
   -l, --listen					Never cease reading input.
   -d, --delimitors=DELIM_LEFT,DELIM_RIGHT	Defines the delimitors used for parsing input.
   -p, --parse					Only replace the strings contained between
						DELIM_LEFT and DELIM_RIGHT delimitors.
   -h, --help					Displays this message.

INPUT_SCHEME and OUTPUT_SCHEME possible values are: nag, iast, hk, itrans, slp1. They must be different.
DELIM_LEFT and DELIM_RIGHT must comprehend only one character, and their values must also be different.
EOF
}



#----------------------------------------------------------#
#                   TRANSLIT FUNCTIONS                     #
#----------------------------------------------------------#


replace ()
{
# Simple replacement of a sign by another.

local output_text="$@"

for i in ${subst_indices[@]} ; do
    output_text="${output_text//${IN_SCHEME[$i]}/${OUT_SCHEME[$i]}}"
done

echo "$output_text"
}


tr_from_nag ()
{
# When nāgarī is the input scheme.

local output_text="$@"

for i in {16..48} ; do
    output_text="${output_text//${IN_SCHEME[$i]}/${OUT_SCHEME[$i]}${ADD_SIGNS[0]}}"
done
for i in {0..15} ; do
    output_text="${output_text//${IN_SCHEME[$i]}/${OUT_SCHEME[$i]}}"
done
for i in {50..62} ; do
    output_text="${output_text//${IN_SCHEME[$i]}/${OUT_SCHEME[$((i-49))]}}"
done
for i in ${subst_indices[@]} ; do
    output_text="${output_text//${ADD_SIGNS[0]}${OUT_SCHEME[$i]}/${OUT_SCHEME[$i]}}"
done
    output_text="${output_text//${ADD_SIGNS[0]}${IN_SCHEME[63]}}"
    output_text="${output_text//${IN_SCHEME[49]}/${ADD_SIGNS[2]}}"
    output_text="${output_text//${ADD_SIGNS[0]}/${OUT_SCHEME[0]}}"
for i in {64..73} ; do
    output_text="${output_text//${IN_SCHEME[$i]}/${ADD_SIGNS[$((i-61))]}}"
done
    output_text="${output_text//${IN_SCHEME[75]}/${ADD_SIGNS[14]}}"
    output_text="${output_text//${IN_SCHEME[74]}/${ADD_SIGNS[13]}}"

echo "$output_text"
}


tr_to_nag ()
{
# When nāgarī is the output scheme.

local output_text="$@"

for i in ${subst_indices_1[@]} ; do
    output_text="${output_text//${IN_SCHEME[$i]}/${ADD_SIGNS[0]}${OUT_SCHEME[$((i+49))]}}"
done
for i in 14 15 ; do
    output_text="${output_text//${IN_SCHEME[$i]}/${OUT_SCHEME[$i]}}"
done
for i in ${subst_indices_2[@]} ; do
    output_text="${output_text//${IN_SCHEME[$i]}[${ADD_SIGNS[0]}${IN_SCHEME[0]}]/${OUT_SCHEME[$i]}}"
done
for i in ${subst_indices_2[@]} ; do
    output_text="${output_text//${IN_SCHEME[$i]}/${OUT_SCHEME[$i]}${OUT_SCHEME[63]}}"
done
for i in {50..62} ; do
    output_text="${output_text//${ADD_SIGNS[0]}${OUT_SCHEME[$i]}/${OUT_SCHEME[$((i-49))]}}"
done
    output_text="${output_text//[${ADD_SIGNS[1]}${ADD_SIGNS[2]}]/${OUT_SCHEME[49]}}"
    output_text="${output_text//${IN_SCHEME[0]}/${OUT_SCHEME[0]}}"
for i in {3..12} ; do
    output_text="${output_text//${ADD_SIGNS[$i]}/${OUT_SCHEME[$((i+61))]}}"
done
    output_text="${output_text//${ADD_SIGNS[14]}/${OUT_SCHEME[75]}}"
    output_text="${output_text//${ADD_SIGNS[13]}/${OUT_SCHEME[74]}}"

echo "$output_text"
}


up_to_low ()
{
# Converts uppercase letters to lowercase.
# Useful only when IAST is the input scheme.

local output_text="$@"

output_text="${output_text,,}"

# Conversion needs to be done by hand for non-ASCII characters.

for i in 50 52 54 55 56 57 58 63 64 69 74 75 77 79 94 95 ; do
    output_text="${output_text//${IN_SCHEME[$i]}/${IN_SCHEME[$((i-49))]}}"
done

if [ "$OUTPUT_SCHEME" = NAG ] ; then
    tr_to_nag "$output_text"
else
    replace "$output_text"
fi
}


unknown_chars ()
{
# This determines if there are, in the output text, some chars
# that should not be part of it.
# $@ = string to test
# ${OUT_SCHEME[@]} = set of characters from the output scheme

local string="$@"
local chars="$string"
local n

# Removing chars that are part of OUT_SCHEME:
for (( n=0; n<${#OUT_SCHEME[@]}; n++ )) ; do
    chars="${chars//${OUT_SCHEME[$n]}}"
done
# Removing spaces:
    chars="${chars// }"

if [ -n "$chars" ] ; then
    log_notice "some chars have not been transliterated:"
    echo "input text: \`$input_text\`" >&2
    echo "output text: \`$string\`" >&2
    echo "unknown chars: \`$chars\`" >&2
    return 1
else
    return 0
fi
}



#----------------------------------------------------------#
#                     FILES HANDLING                       #
#----------------------------------------------------------#


parse ()
{
# Parses a string with simple markup.
# $@ is a line of text, not a file.

declare -r output_text="$@"
declare -i n_DELIM_L=0 n_DELIM_R=0

while IFS= read -rd "$DELIM_L" ; do
    (( n_DELIM_L++ ))
done <<< "$output_text"

while IFS= read -rd "$DELIM_R" ; do
    (( n_DELIM_R++ ))
done <<< "$output_text"

if [ "$n_DELIM_L" -eq 0 ] && [ "$n_DELIM_R" -eq 0 ] ; then
    echo "$output_text"
    return
elif [ "$n_DELIM_L" -ne "$n_DELIM_R" ] ; then
    if [ "$n_DELIM_L" -gt "$n_DELIM_R" ] ; then
        echo "$SCRIPT_NAME: markup error! there is more \"$DELIM_L\" than \"$DELIM_R\"."
    else
        echo "$SCRIPT_NAME: markup error! there is more \"$DELIM_R\" than \"$DELIM_L\"."
    fi
    echo ">>> $output_text" >&2
    exit 1
fi

while IFS="$DELIM_L" read -rd "$DELIM_R" NON_SKT SKT ; do
    echo -n $NON_SKT
    echo -n "$($translit_mode '$SKT')"
done <<< "$output_text"
    echo "${output_text##*$DELIM_R}"
}


parse_all ()
{
# $@ is a file, not a line of text.

local xml_file="$@"

while IFS='<' read -rd '>' str markup ; do
    if [ "$str" ] ; then
        for i in {0..10..2} ; do
            str="${str//${XML[$((i+1))]}/${XML[$i]}}"
        done
        str="$(parse '$str')"
        for i in {0..10..2} ; do
            str="${str//${XML[$i]}/${XML[$((i+1))]}}"
        done
        echo -n "$str"
    fi
    echo -n "<$markup>"
done < "$xml_file"

echo
}



#----------------------------------------------------------#
#                  PARAMETERS CHECKING                     #
#----------------------------------------------------------#


check_opt ()
{
if [[ $1 =~ = ]] ; then
    set -- ${1%=*} ${1#*=} ${@/$1}
fi
if [[ $2 =~ ^-|^-- ]] ; then
    log_error "$1 option needs an argument."
else
    echo $@
fi
}


if [ -z "$1" ] ; then
    usage
    exit 1
else
    until [ -z "$1" ] ; do
        case $1 in
            -i|--input-scheme=*)
                i_flag=1
                set -- $(check_opt $@)
                INPUT_SCHEME=$2
		shift
                ;;
            -o|--output-scheme=*)
                o_flag=1
                set -- $(check_opt $@)
                OUTPUT_SCHEME=$2
		shift
                ;;
            -l|--listen)
                l_flag=1
                ;;
            -p|--parse)
                p_flag=1
                ;;
            -d|--delimitors=*)
                d_flag=1
                set -- $(check_opt $@)
                DELIMITORS="$2"
		shift
                ;;
            -h|--help)
                usage
                exit 0
                ;;
            -v|--version)
                version
                exit 0
                ;;    
            --)
                shift
                arg_text="$@"
                break
                ;;
            --*|-?)
                log_error "unknown option: \"$1\"."
                ;;
            -?*)
                while IFS= read -rn 1 c ; do
                    [ -n "$c" ] && splitted[$((i++))]="-$c"
                done <<< ${1#-} 
                shift
                set -- "${splitted[@]} $@"
                ;;
            *)
                arg_text="$@"
                break
                ;;
        esac
    shift
    done
fi

# Converting $INPUT_SCHEME and $OUTPUT_SCHEME to uppercase.

INPUT_SCHEME=${INPUT_SCHEME^^}
OUTPUT_SCHEME=${OUTPUT_SCHEME^^}

# Some tests. So far, we have not verified if arguments provided on the command line
# contain valid data.

if [ -z "$INPUT_SCHEME" ] || [ -z "$OUTPUT_SCHEME" ] ; then
    log_error "-i/--input-scheme and -o/--output-scheme must be defined."
fi
if [[ -z "$arg_text" || "$arg_text" = '-' ]] && [ "$l_flag" != 1 ] ; then
    if ! [ -t 0 ] ; then
        r_STDIN=1
    else
        log_error "a text string or a filename must be supplied on the command line when the -l/--listen option is not activated."
    fi
fi
if [ "$INPUT_SCHEME" ] && ! grep -qw "$INPUT_SCHEME" <<< "$SCHEMES" ; then
    log_error "the input scheme \"$INPUT_SCHEME\" is not valid."
fi
if [ "$OUTPUT_SCHEME" ] && ! grep -qw "$OUTPUT_SCHEME" <<< "$SCHEMES" ; then
    log_error "the output scheme \"$OUTPUT_SCHEME\" is not valid."
fi
if [ "$INPUT_SCHEME" = "$OUTPUT_SCHEME" ] ; then
    log_error "input scheme and output scheme must be different."
fi
if [ "$d_flag" = 1 ] ; then
    if [ "${#DELIMITORS}" -ne 3 ] ; then
        log_error "the delimitors indications (i.e.: \`$DELIMITORS\`) are incorrect."
    else
        DELIM_L="${DELIMITORS:0:1}" DELIM_R="${DELIMITORS:2:1}"
        if [ "$DELIM_L" = "$DELIM_R" ] ; then
            log_error "the delimitors used for parsing must be different."
        fi
    fi
fi



#----------------------------------------------------------#
#                TRANSLIT RULES CHECKING                   #
#----------------------------------------------------------#

case $INPUT_SCHEME in
  HK)
    IN_SCHEME=( ${HK[@]} )
    case $OUTPUT_SCHEME in
      IAST)
        OUT_SCHEME=( ${IAST[@]} )
        subst_indices=( 9 8 7 27 29 1 3 5 6 14 15 20 25 26 28 30 45 46 )
        translit_mode=replace
        ;;
      ITRANS)
        OUT_SCHEME=( ${ITRANS[@]} )
        subst_indices=( 9 7 8 22 1 3 5 6 20 21 25 45 46 )
        translit_mode=replace
        ;;
      NAG)
        OUT_SCHEME=( ${NAG[@]} )
        subst_indices_1=( 9 7 8 11 13 1 2 3 4 5 6 10 12 )
        subst_indices_2=( 17 19 22 24 27 29 32 34 37 39 16 18 20 21 23 25 26 28 30 31 33 35 36 38 {40..48} )
        translit_mode=tr_to_nag
        ;;
      SLP1)
        OUT_SCHEME=( ${SLP1[@]} )
        subst_indices=( 9 7 8 27 29 6 11 13 17 19 20 22 24 25 26 28 30 32 34 37 39 45 46 )
        translit_mode=replace
        ;;
    esac
    ;;
  IAST)
    IN_SCHEME=( ${IAST[@]} )
    translit_mode=up_to_low
    case $OUTPUT_SCHEME in
      HK)
        OUT_SCHEME=( ${HK[@]} )
        subst_indices=( 27 29 1 3 5 6 7 8 9 14 15 20 25 26 28 30 45 46 )
        ;;
      ITRANS)
        OUT_SCHEME=( ${ITRANS[@]} )
        subst_indices=( 22 27 29 1 3 5 6 7 8 9 14 15 20 21 25 26 28 30 45 46 )
        ;;
      NAG)
        OUT_SCHEME=( ${NAG[@]} )
        subst_indices_1=( 11 13 {1..12} )
        subst_indices_2=( 17 19 22 24 27 29 32 34 37 39 16 18 20 21 23 25 26 28 30 31 33 35 36 38 {40..48} )
        ;;
      SLP1)
        OUT_SCHEME=( ${SLP1[@]} )
        subst_indices=( 27 29 1 3 5 6 7 8 9 11 13 14 15 17 19 20 22 24 25 26 28 30 32 34 37 39 45 46 )
        ;;
    esac
    ;;
  ITRANS)
    IN_SCHEME=( ${ITRANS[@]} )
    case $OUTPUT_SCHEME in
      HK)
        OUT_SCHEME=( ${HK[@]} )
        subst_indices=( 1 3 5 6 7 8 9 20 21 22 25 45 46 )
        translit_mode=replace
        ;;
      IAST)
        OUT_SCHEME=( ${IAST[@]} )
        subst_indices=( 27 29 1 3 5 6 7 8 9 14 15 20 21 22 25 26 28 30 45 46 )
        translit_mode=replace
        ;;
      NAG)
        OUT_SCHEME=( ${NAG[@]} )
        subst_indices_1=( 1 3 5 6 7 8 9 11 13 2 4 10 12 )
        subst_indices_2=( 17 19 22 24 27 29 32 34 37 39 16 18 20 21 23 25 26 28 30 31 33 35 36 38 {40..48} )
        translit_mode=tr_to_nag
        ;;
      SLP1)
        OUT_SCHEME=( ${SLP1[@]} )
        subst_indices=( 27 29 1 3 5 6 7 8 9 11 13 17 19 20 21 22 24 25 26 28 30 32 34 37 39 45 46 )
        translit_mode=replace
        ;;
    esac
    ;;
  NAG)
    IN_SCHEME=( ${NAG[@]} )
    translit_mode=tr_from_nag
    case $OUTPUT_SCHEME in
      HK)
        OUT_SCHEME=( ${HK[@]} )
        subst_indices=( 11 13 9 8 7 {1..6} 10 12 )
        ;;
      IAST)
        OUT_SCHEME=( ${IAST[@]} )
        subst_indices=( 11 13 {1..10} 12 )
        ;;
      ITRANS)
        OUT_SCHEME=( ${ITRANS[@]} )
        subst_indices=( 11 13 3 8 6 1 2 4 5 7 9 10 12 )
        ;;
      SLP1)
        OUT_SCHEME=( ${SLP1[@]} )
        subst_indices=( {1..13} )
        ;;
    esac
    ;;
  SLP1)
    IN_SCHEME=( ${SLP1[@]} )
    case $OUTPUT_SCHEME in
      HK)
        OUT_SCHEME=( ${HK[@]} )
        subst_indices=( 6 7 8 9 11 13 17 19 20 22 24 25 26 27 28 29 30 32 34 37 39 45 46 )
        translit_mode=replace
        ;;
      IAST)
        OUT_SCHEME=( ${IAST[@]} )
        subst_indices=( 1 3 5 6 7 8 9 11 13 14 15 17 19 20 22 24 25 26 27 28 29 30 32 34 37 39 45 )
        translit_mode=replace
        ;;
      ITRANS)
        OUT_SCHEME=( ${ITRANS[@]} )
        subst_indices=( 1 3 5 6 7 8 9 11 13 17 19 20 21 22 24 25 26 27 28 29 30 32 34 37 39 45 46 )
        translit_mode=replace
        ;;
      NAG)
        OUT_SCHEME=( ${NAG[@]} )
        subst_indices_1=( {1..13} )
        subst_indices_2=( {16..48} )
        translit_mode=tr_to_nag
        ;;
    esac
    ;;
esac



#----------------------------------------------------------#
#                LAUNCHING OF THE PROGRAM                  #
#----------------------------------------------------------#


if [ "$p_flag" = 1 ] ; then
    translit_method="parse"
elif [ "$P_flag" = 1 ] ; then
    translit_method="parse_all"
else
    translit_method="$translit_mode"
fi

if [ -f "$arg_text" ] ; then
    if ! [ -r "$arg_text" ] ; then
        log_error "the file \"${arg_text##*/}\" is not readable."
    else
        log_notice "reading file \"${arg_text##*/}\"."
        if [ "$P_flag" = 1 ] ; then
            $translit_method "$arg_text"                                ##### CHECK THIS
        else
            while IFS= read -r input_text  || [ "$input_text" ] ; do
                output_text="$($translit_method '$input_text')"
                unknown_chars "$output_text" && echo "$output_text"
            done < "$arg_text"
        fi
    fi
else
    if [ "$l_flag" = 1 ] ; then
        log_notice "press [Ctrl + c] to leave the program."
        if [ "$arg_text" ] ; then
            input_text="$arg_text"
            output_text="$($translit_method '$input_text')"
            unknown_chars "$output_text" && echo "$output_text"
        fi
        while IFS= read -r input_text || [ "$input_text" ] ; do
            output_text="$($translit_method '$input_text')"
            unknown_chars "$output_text" && echo "$output_text"
        done
    elif [ "$r_STDIN" = 1 ] ; then
        log_notice "reading stdin."
        cat - | while IFS= read -r input_text  || [ "$input_text" ] ; do
            output_text="$($translit_method '$input_text')"
            unknown_chars "$output_text" && echo "$output_text"
        done
    else
        input_text="$arg_text"
        output_text="$($translit_method "$input_text")"
        unknown_chars "$output_text" && echo "$output_text"
    fi
fi


